package rayCaster;

import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;
import primitive3D.Cube;
import primitive3D.Pyramid;
import primitive3D.Sphere;
import static rayCaster.ApplicationInterface.*;
import helper.Stopwatch;
import static helper.Util.*;
import static java.lang.Math.*;


public class RayCasterApplication {
	private String outputFile;
	private String inputFile;
	private int width;
	private int height;
	private double fov;
	private COLOR_OPTIONS colorMode;
	private COLOR_VAR_OPTIONS colorVariationMode;
	private String outputFormat;
	private boolean timeMeasuringMode;
	private RayCaster raycaster;
	
	public RayCasterApplication() {
		//Set default values
		inputFile = DEFAULT_SCENE;		
		outputFile = OUT_FILE;
		outputFormat = DEFAULT_EXTENSION;
		width = WIDTH;
		height = HEIGHT;
		fov = VISUAL_ANGLE;
		colorMode = COLOR_OPTIONS.RANDOM;
		colorVariationMode = COLOR_VAR_OPTIONS.LINEAR;		
		timeMeasuringMode = false;		
		raycaster = new RayCaster();		
	}
	
	public int getWidth(){
		return width;
	}
	
	public int getHeigth(){
		return height;		
	}
	
	public double getFov(){
		return fov;
	}
	
	public String getOutputFileName(){
		return outputFile;
	}
	
	public String getInputFileName(){
		return inputFile;
	}
	
	public String getOutputFormat(){
		return outputFormat;
	}
	public RayCaster getCaster(){		
		return raycaster;
	}
		
	public COLOR_OPTIONS getColorMode(){		
		return colorMode;
	}

	public COLOR_VAR_OPTIONS getColorVariationMode(){		
		return colorVariationMode;
	}
	
	//For debugging
	public String toString(){		
		StringBuffer str = new StringBuffer();		
		str.append("Application Parameters :  \n");
		str.append("input = "+inputFile+"\toutput = "+outputFile+"\t\toutputFormat = "+outputFormat+"\n");
		str.append("width = "+width+"\theight = "+height+"\n");
		str.append("fov = "+fov+"\n");
		str.append("colormode = "+colorMode+"\tcolorVariationMode="+colorVariationMode+"\n");
		str.append("time = " + timeMeasuringMode+"\n");
		return str.toString();
	}
	
	public boolean parseCommandLine( String[] args){
		String stringValue = null;	
		
		for(int i=0 ; i<args.length; i++){
			if( args[i].charAt(0) == '-' ){				 
				int index = getParamIndex( args[i].substring(1) );
				String option = args[i];
				if( index == -1 ){
					System.out.println("El switch "+option+" no es valido.");
					return false;
				}
					
				if( isUnarySwitch(index) )
					stringValue = null;
				else{
					if( args[i+1].charAt(0) == '-'){
						System.out.println("El switch " + option + " esperaba un parametro.");
						return false;
					}else{
						stringValue = args[i+1].substring(0);
						i++;
					}
				}
				
				switch( index ){
					case 0: inputFile = new String(stringValue);
							int pos =  stringValue.lastIndexOf('.');
							if( pos == 0 || pos == -1 || pos == stringValue.length()-1 ){								
								System.out.println("Nombre de archivo invalido.");
								return false;
							}
							outputFile = new String(stringValue.substring(0,pos));
							break;
					case 1: if( processNameAndExtension(stringValue) == false )
								return false;
							break;
					case 2: String[] dimensions = stringValue.split("x");							
							width = Integer.parseInt(dimensions[0].trim());
							height = Integer.parseInt(dimensions[1].trim()); break;
					case 3: fov = Double.parseDouble(stringValue.trim()); break;
					case 4: setColorModeField( stringValue ); break;
					case 5: setColorVariationModeField( stringValue ); break;
					case 6: timeMeasuringMode = true; break;					
				}				
			}else{
				System.out.println("Error en la sintaxis del comando");
				return false;
			}
		}
		return true;
	}
	
	
	
	private boolean processNameAndExtension(String stringValue){
		int pos =  stringValue.lastIndexOf('.');
		if( pos == 0 || pos == stringValue.length()-1 ){								
			System.out.println("Nombre de archivo invalido.");
			return false;
		}	
		
		String ext = null;
		String name = null;
		if( pos == -1 ){
			ext = DEFAULT_EXTENSION;
			name = stringValue;
		}else{
			ext = stringValue.substring(pos+1, stringValue.length());
			name = stringValue.substring(0,pos);
		}					 	
	 	
	 	if( !ext.isEmpty() ){
	 		if( ext.compareToIgnoreCase("bmp") != 0 && ext.compareToIgnoreCase("png") != 0 ){
	 			System.out.println("Solo los formatos bmp y png son soportados");
	 			return false;
	 		}
	 		else{
	 			if( !isValidString(ext) ){
	 				System.out.println("Extension del archivo de salida invalido");
		 			return false;
	 			}
	 			outputFormat = new String(ext);
	 		}
	 	}
	 		
	 	if( !name.isEmpty()){
	 		if( !isValidString(name) ){
	 			System.out.println("Nombre de archivo de salida invalido");
	 			return false;
	 		}						 			
	 		outputFile = new String(name);
	 	}
	 	return true;
	}
	
	
	
	private boolean isValidString(String str){
		boolean isValid = true;
		for( int j=0 ; isValid && j<str.length() ; j++ ){
 			char c = str.charAt(j);
 			if( !Character.isLetterOrDigit(c) && c != '_' && c != '.' )
 				isValid = false;
 		}
		return isValid;
	}
	private int getParamIndex(String string) {
		
		for(int i=0 ; i< validSwitches.length ; i++){
			if( string.compareToIgnoreCase( validSwitches[i] ) == 0 ) 
					return i;
		}
		return -1;
	}
	
	private boolean isUnarySwitch(int index) {		
		return index == 6;
	}
	
	private void setColorVariationModeField(String stringValue) {
		if( stringValue.compareToIgnoreCase("linear") == 0 )
			colorVariationMode = COLOR_VAR_OPTIONS.LINEAR;
		else if( stringValue.compareToIgnoreCase("log") == 0 )
			colorVariationMode = COLOR_VAR_OPTIONS.LOG;
	}
	
	private void setColorModeField(String stringValue) {
		if( stringValue.compareToIgnoreCase("random") == 0 )
			colorMode = COLOR_OPTIONS.RANDOM;
		else if( stringValue.compareToIgnoreCase("ordered") == 0 )
			colorMode = COLOR_OPTIONS.ORDERED;
	}
	

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		//Timer to measure how long it takes to prepare the caster
		Stopwatch preparationTimer = new Stopwatch().start();
		
		RayCasterApplication app = new RayCasterApplication();
		if( !app.parseCommandLine(args)){
			showUsage();
			return;
		}		
		System.out.println(app);
		
		Camera camera = new Camera(app.getWidth(),app.getHeigth(), app.getFov() );
		Scene scene = null;
		try {
			scene = loadScene(app.getInputFileName());
		} catch (Exception e) {
			System.out.println("Caster Application : main1 : " + e.getMessage());
		}
		int width = app.getWidth();
		int height = app.getHeigth();
				
		Pixel[] image;		
		try {
			RayCaster caster = app.getCaster();		
			preparationTimer.stop();
			//How long the render takes
			Stopwatch renderTimer = new Stopwatch().start();
			image = caster.rayCast(camera, scene, width, height,
					app.getColorMode(), app.getColorVariationMode());
			
			renderTimer.stop();
			Stopwatch ioTimer = new Stopwatch().start();			
			if( !saveImage(image, width, height, app.getOutputFileName(), app.getOutputFormat()) )
				System.out.println("Caster Application : Errors while trying to save image");
			ioTimer.stop();
			app.showTimerResults(preparationTimer, renderTimer, ioTimer);			
			System.out.println("Done !!\n");
		} catch (Exception e) {
			System.out.println("Caster Application : main2 : "  + e.getMessage());
			
			return;
		}	
		
	}

	public void showTimerResults(Stopwatch preparationTimer, Stopwatch renderTimer, Stopwatch ioTimer) {
		if( timeMeasuringMode ){
			System.out.println("Time Results :");
			System.out.println("Preparation Time = " + preparationTimer.getElapsedTime() + " ms");
			System.out.println("Render Time = " + renderTimer.getElapsedTime() + " ms");
			System.out.println("File IO Time = " + ioTimer.getElapsedTime() + " ms");
		}		
	}
	
	//For building scenes	
	private static Scene loadScene(String inputFilename) throws Exception {		
		Scene scene = new Scene();		
		boolean sceneFound = false;
		
		for( int i=0 ; !sceneFound && i< scenesList.length; i++){				
			if( inputFilename.compareToIgnoreCase( scenesList[i] ) == 0 ){				
				sceneFound = true;
				switch(i){
				case 0 : buildSceneOne( scene ); break;
				case 1 : buildSceneTwo( scene ); break;
				case 2 : buildSceneThree( scene ); break;				
				}
			}				
		}
		if( !sceneFound ){
			System.out.println("Unknown scene. Scene 1 will be rendered !!");
			buildSceneOne( scene );
		}
		return scene;
	}
	
	private static void buildSceneOne(Scene scene) throws Exception {
		//Central pyramid with side 5
		Pyramid pyramid = new Pyramid();
		pyramid.scale(5);
		pyramid.setPosition(new Point3d(0,0,5.0/3.0));
		
		//Central sphere just above the pyramid
		Sphere centralSphere = new Sphere();
		centralSphere.setPosition(new Point3d(0, 0, 6));
			
		
		//4 spheres at distance 3 from the pyramid vertices
		Sphere sphere1=null;
		Sphere sphere2=null;
		Sphere sphere3=null;
		Sphere sphere4=null;
		
		sphere1 = new Sphere(0.5);
		sphere2 = new Sphere(0.5);
		sphere3 = new Sphere(0.5);
		sphere4 = new Sphere(0.5);			
		
		/* The four spheres are positioned in a square of vertices that
		 * are the vertices of the pyramid base but scaled. So, we
		 * calculate the distance from origin to the vertices secaled
		 * and the distance for coordinates X and Y are the rectangular
		 * components
		 */
		//double distFromOrigin = (5*sqrt(2)/2) + 3;
		//double dist = distFromOrigin/sqrt(2);
		double dist = (3*sqrt(2)/2);
		
		sphere1.setPosition(new Point3d( dist, dist, 0.5 ) );
		sphere2.setPosition(new Point3d( -dist, dist, 0.5 ) );
		sphere3.setPosition(new Point3d( dist, -dist, 0.5) );
		sphere4.setPosition(new Point3d( -dist, -dist, 0.5) );
		
		
		//Add all objects
		scene.add(pyramid);
		scene.add(centralSphere);
		scene.add(sphere1);
		scene.add(sphere2);
		scene.add(sphere3);
		scene.add(sphere4);
		return;
	}
	
	
	private static void buildSceneTwo(Scene scene) throws Exception {		
		/* 4 spheres with radius 0.5 separated 1.5 from each other
		 * That means 4 points separated 1.5 => 3*1.5 = 4.5
		 * Then the position of the most upper left should be 4.5/2 = 2.25
		 */
		int cantSpheresPerSide = 4;
		double sphereRadius = 0.5;
		double separation = 1.5;		
		int jumps = cantSpheresPerSide-1;
		
		double totalDistance = jumps*separation;
		double coordinateInitialNumber = totalDistance/2;
		
		Point3d startingPoint = new Point3d(-coordinateInitialNumber, coordinateInitialNumber, -coordinateInitialNumber);
		Vector3d deltaX = new Vector3d(separation, 0, 0);
		Vector3d deltaY = new Vector3d(0, -separation, 0);
		Vector3d deltaZ = new Vector3d(0, 0, separation);
		
		for( int k=0 ; k<cantSpheresPerSide ; k++ ){
			Point3d pivoteZ = new Point3d(startingPoint);
			pivoteZ.scaleAdd(k, deltaZ, startingPoint);
			for( int i=0 ; i<cantSpheresPerSide ; i++ ){
				Point3d pivoteY = new Point3d();
				pivoteY.scaleAdd(i, deltaY, pivoteZ);
				for( int j=0 ; j<cantSpheresPerSide ; j++ ){					
						Point3d currentPoint = new Point3d();
						currentPoint.scaleAdd(j, deltaX, pivoteY);												
						scene.add(new Sphere(currentPoint,sphereRadius));
				}				
			}
		}
		
	}
	
	private static void buildSceneThree(Scene scene) throws Exception {
		double side = 2;
		double radius = 1;
		double separation = 0.5;
		//The cube base must lay on the XY plane
		Cube centralCube = new Cube(side,new Point3d(0,0,side/2));
				
		//Sphere must lay on the XY plane
		double rightSpherePosX = side/2+radius+separation;  
		Sphere rightSphere = new Sphere(new Point3d(rightSpherePosX, 0 , radius/2),radius);
		
		double rightCubePosX = rightSpherePosX + radius + side/2+separation;
		Cube rightCube = new Cube(side,new Point3d(rightCubePosX, 0, side/2));
		
		Sphere leftSphere = new Sphere( new Point3d(-rightSpherePosX, 0, radius/2), radius);
		Cube leftCube = new Cube( side, new Point3d(-rightCubePosX, 0, side/2));
		
		scene.add(centralCube);
		scene.add(leftSphere);
		scene.add(leftCube);
		scene.add(rightSphere);
		scene.add(rightCube);		
	}
	private static void showUsage(){
		System.out.println("USAGE:");
		System.out.println("======\n");
		System.out.println("raycaster -i < scene1.sc | scene2.sc | scene3.sc >  [-o outputFileName.extension ] ");
		System.out.println("\t[-size widthxheight]  [-fov angle]  [-cm random|ordered] [-cv linear|log]");
		System.out.println("\nExample:  raycaster -i scene2.sc -o render2.png -size 640x480 -fov 20 -cm random -cd log");
	}

}
